39.控制结构 ( if-else语句 、for循环 、while循环)

本章概要

  • 学习材料:税率边界、价格信号和迭代序列。
  • 本章任务:运行 lst-if-salary-taxlst-break-continue,形成分支跟踪和循环终止记录。
  • 完成后你将得到:边界真值表、逐步输出、break/continue命中位置。
  • 自我检查:测试边界两侧与等号点;while无更新或分支重叠时先检查原因。
  • 拓展练习:把控制流拓展应用到中国授信审批规则。

本章概览:程序的三种基本控制结构

控制流(Control Flow) 决定了代码的执行顺序和逻辑分支。

根据 Bohm-Jacopini 定理,程序有三种基本控制结构:

结构 描述 关键字
顺序结构 语句按顺序依次执行 (默认)
选择结构 根据条件选择执行路径 if-elif-else
循环结构 重复执行某段代码 for / while

控制结构在金融中的核心应用

  • 状态判断顺序:条件判断 RULE_A/ALERT/REVIEW
  • 风险控制:越界时停止自动流程并进入人工核对
  • 批量处理:遍历股票列表进行分析
  • 迭代优化:梯度下降法、牛顿法求解

if-else 选择结构

if-else选择结构流程 展示if-elif-else条件判断的逻辑流程 开始 条件判断? True / False 执行代码块A True 执行代码块B False 继续执行

布尔逻辑:条件表达式的基础

布尔(Boolean)代数是条件判断的数学基础:

运算 语法 含义
与(AND) A and B 两者都真才真
或(OR) A or B 至少一个真就真
非(NOT) not A 真变假,假变真

短路求值(Short-circuit Evaluation)

  • A and B:如果 A 为假,不计算 B
  • A or B:如果 A 为真,不计算 B

案例:个人所得税累进税率计算

累进税率是现代税收制度的核心,体现’量能课税’原则:

  • 超额累进:只对超过该级的部分征税
  • 速算扣除数:简化分段计算
  • 起征点:5000元免征额,保护低收入群体

核心公式:

\[ \large \begin{aligned} \text{应纳税额} &= \text{应纳税所得额} \times \text{税率} \\ &\quad - \text{速算扣除数} \end{aligned} \]

if-elif-else 结构:工资扣税计算

Listing 1: 工资扣税计算(累进税率)
展开完整参考实现
def calculate_tax(salary):
    """
    计算个人所得税(简化版)
    中国个人所得税采用7级超额累进税率
    """
    # 使用if-elif-else匹配累进税率表
    if salary <= 3000:
        rate, deduction = 0, 0
    elif salary <= 12000:
        rate, deduction = 0.03, 0
    elif salary <= 25000:
        rate, deduction = 0.10, 210
    elif salary <= 35000:
        rate, deduction = 0.20, 1410
    elif salary <= 55000:
        rate, deduction = 0.25, 2660
    elif salary <= 80000:
        rate, deduction = 0.30, 4410
    else:
        rate, deduction = 0.35, 7160

    # 应纳税所得额 = 税前工资 - 起征点(5000元)
    taxable_income = salary - 5000
    # 应纳税额 = 应纳税所得额 × 税率 - 速算扣除数
    tax = max(0, taxable_income * rate - deduction)
    after_tax = salary - tax
    return tax, after_tax

工资扣税:确定性情景核对

Listing 2
展开工资扣税示例输出
# 测试不同收入水平
salaries = [2500, 5000, 15000, 30000, 60000, 100000]
print('工资扣税计算:')
print('-' * 60)
for salary in salaries:
    tax, after_tax = calculate_tax(salary)
    tax_rate = tax / salary * 100 if salary > 0 else 0
    print(f'税前工资: {salary:8.2f}元 | 税额: {tax:7.2f}元 | 税率: {tax_rate:5.2f}% | 税后: {after_tax:8.2f}元')
工资扣税计算:
------------------------------------------------------------
税前工资:  2500.00元 | 税额:    0.00元 | 税率:  0.00% | 税后:  2500.00元
税前工资:  5000.00元 | 税额:    0.00元 | 税率:  0.00% | 税后:  5000.00元
税前工资: 15000.00元 | 税额:  790.00元 | 税率:  5.27% | 税后: 14210.00元
税前工资: 30000.00元 | 税额: 3590.00元 | 税率: 11.97% | 税后: 26410.00元
税前工资: 60000.00元 | 税额: 12090.00元 | 税率: 20.15% | 税后: 47910.00元
税前工资: 100000.00元 | 税额: 26090.00元 | 税率: 26.09% | 税后: 73910.00元

if-elif-else 的执行逻辑

  • elif 是 ‘else if’ 的缩写
  • 条件从上到下依次判断
  • 一旦某个条件为真,执行对应分支后立即跳出
  • 各分支是互斥关系,只有一个会被执行
if salary <= 3000:       # 第1级
    rate = 0
elif salary <= 12000:    # 第2级(隐含 salary > 3000)
    rate = 0.03
elif salary <= 25000:    # 第3级(隐含 salary > 12000)
    rate = 0.10
else:                    # 兜底(以上条件都不满足)
    rate = 0.35

嵌套 if 与逻辑运算:两种写法对比

当需要同时满足多个条件时:

写法 代码 适用场景
嵌套 if if A:if B: 条件有层级关系
逻辑运算 if A and B: 条件独立并列

选择建议:代码可读性优先

案例:多字段核对状态生成

课堂规则逻辑(多条件嵌套判断):

  • RULE_A:均线顺序 + 数值越界 + 成交量条件 + RSI条件
  • RULE_B:反向均线顺序 + 反向越界 + 成交量条件 + RSI条件
  • REVIEW:其他情况或依据不足

中性规则状态代码实现

Listing 3: 多字段核对状态生成系统
展开完整参考实现
def generate_signal(price, ma20, ma60, volume, avg_volume, rsi):
    """基于课堂数值字段的多条件嵌套判断"""
    signal = 'REVIEW'
    reason = []

    # 第一层:判断市场趋势(均线排列)
    if ma20 > ma60:
        reason.append('均线多头')
        # 第二层:价格是否突破均线
        if price > ma20:
            reason.append('突破20日均线')
            # 第三层:量价配合 + RSI确认
            if volume > avg_volume * 1.2 and rsi < 70:
                signal = 'RULE_A'
                reason.append('放量上涨')
    elif ma20 < ma60:
        reason.append('均线空头')
        if price < ma20:
            reason.append('跌破20日均线')
            if volume > avg_volume * 1.2 and rsi > 30:
                signal = 'RULE_B'
                reason.append('放量下跌')
    else:
        reason.append('均线接近,进入核对')

    return signal, '; '.join(reason)

条件信号:情景输出核对

Listing 4
展开条件信号示例输出
# 测试不同市场情景
signals = [
    (18.5, 18.0, 17.5, 2500000, 2000000, 45),
    (18.5, 18.0, 17.5, 1500000, 2000000, 65),
    (18.5, 18.0, 17.5, 2500000, 2000000, 75),
    (16.5, 18.0, 17.5, 2500000, 2000000, 25),
]
print('课堂规则状态分析:')
print('-' * 80)
for price, ma20, ma60, volume, avg_vol, rsi in signals:
    signal, reason = generate_signal(price, ma20, ma60, volume, avg_vol, rsi)
    print(f'价格={price:.1f}, MA20={ma20:.1f}, MA60={ma60:.1f}, 状态={signal}, 原因=[{reason}]')
课堂规则状态分析:
--------------------------------------------------------------------------------
价格=18.5, MA20=18.0, MA60=17.5, 状态=RULE_A, 原因=[均线多头; 突破20日均线; 放量上涨]
价格=18.5, MA20=18.0, MA60=17.5, 状态=REVIEW, 原因=[均线多头; 突破20日均线]
价格=18.5, MA20=18.0, MA60=17.5, 状态=REVIEW, 原因=[均线多头; 突破20日均线]
价格=16.5, MA20=18.0, MA60=17.5, 状态=REVIEW, 原因=[均线多头]

结果说明:上述程序结果只用于课堂判断。实际应用还需要检查时间、复权和缺失值,并比较样本外表现、交易成本和不确定性,不能直接把程序输出当作投资指令。

嵌套 if 的执行流程解析

嵌套if决策树 展示多字段规则生成核对状态的三层嵌套判断过程 第1层: MA20 > MA60? True 第2层: Price > MA20? True 第3层: 放量+RSI? RULE_A REVIEW REVIEW False (MA20 < MA60) 反向条件? → RULE_B RULE_B

第二部分:for 循环

for 循环用于遍历序列中的每个元素:

for element in sequence:
    # 对每个element执行操作
    pass  # 此处留给学习者填写逐项处理逻辑

range() 函数生成整数序列:

用法 说明 示例
range(stop) 0 到 stop-1 range(5) → 0,1,2,3,4
range(start, stop) start 到 stop-1 range(1,5) → 1,2,3,4
range(start, stop, step) 自定义步长 range(0,10,2) → 0,2,4,6,8

案例:复利终值计算

复利是金融学的核心概念:利息产生利息。

\[ \large FV = PV \times (1 + r)^n \]

  • \(FV\):终值(Future Value)
  • \(PV\):现值(Present Value)
  • \(r\):利率
  • \(n\):期数

for 循环实现复利计算

观察重点:循环逐年将利息计入余额,再与单利基准比较。

Listing 5: for循环遍历与累计计算
展开逐年复利计算代码
import numpy as np

# ===== 案例1:复利终值计算 =====
principal = 10000  # 本金
rate = 0.05        # 年利率5%
years = 10         # 投资10年

print('复利增长过程(打印第1年与偶数年以控制输出高度):')  # 输出演示标题并说明抽样口径
print(f'{"年份":<6}{"利息":<12}{"年末余额":<12}')  # 输出表头,列宽用格式符对齐

amount = principal
# range(1, years+1) 生成 [1,2,...,10],循环把每年利息计入余额
for year in range(1, years + 1):
    interest = amount * rate        # 当年利息
    amount += interest              # 累加到余额
    if year == 1 or year % 2 == 0:  # 仅输出首年与偶数年,保证结论行不被折叠隐藏
        print(f'{year:<6}{interest:<12.2f}{amount:<12.2f}')  # 输出当年利息与年末余额

# 复利 vs 单利对比
print(f'\n复利终值: {amount:.2f}元')
simple_interest = principal * (1 + rate * years)
print(f'单利对比: {simple_interest:.2f}元')
print(f'复利优势: {amount - simple_interest:.2f}元')
复利增长过程(打印第1年与偶数年以控制输出高度):
年份    利息          年末余额        
1     500.00      10500.00    
2     525.00      11025.00    
4     578.81      12155.06    
6     638.14      13400.96    
8     703.55      14774.55    
10    775.66      16288.95    

复利终值: 16288.95元
单利对比: 15000.00元
复利优势: 1288.95元

案例:年化波动率计算

统一口径:以下固定序列仅用于课堂计算演示,不是市场样本;手算与 NumPy 均使用样本标准差(ddof=1)。

Listing 6: 手动计算标准差与年化波动率
展开波动率计算代码
import numpy as np  # 导入数值运算工具以核对手算结果
daily_returns = [0.02, -0.01, 0.03, -0.02, 0.01, 0.04, -0.03, 0.02, -0.01, 0.01]  # 设置非市场观测的固定课堂演示序列
sample_size = len(daily_returns)  # 记录样本观测数以使用 n-1 分母
mean_daily_return = sum(daily_returns) / sample_size  # 计算日收益率样本均值
squared_deviation_sum = sum((daily_return - mean_daily_return) ** 2 for daily_return in daily_returns)  # 累计离差平方和
sample_variance = squared_deviation_sum / (sample_size - 1)  # 按 ddof=1 口径计算样本方差
annualization_factor = np.sqrt(252)  # 按252个交易日构造年化因子
manual_annualized_volatility = np.sqrt(sample_variance) * annualization_factor  # 将手算样本标准差年化
numpy_annualized_volatility = np.std(daily_returns, ddof=1) * annualization_factor  # 用相同样本口径调用 NumPy 核对
calculation_difference = abs(manual_annualized_volatility - numpy_annualized_volatility)  # 计算两种路径的绝对差异
print(f'样本口径: n={sample_size}, ddof=1, 年化因子=252')  # 显示可核对的统计口径
print(f'手算={manual_annualized_volatility:.2%} | NumPy={numpy_annualized_volatility:.2%} | 差异={calculation_difference:.2e}')  # 紧凑呈现一致的年化结果
样本口径: n=10, ddof=1, 年化因子=252
手算=36.04% | NumPy=36.04% | 差异=0.00e+00

列表推导式:简洁的循环创建列表

# 传统for循环
squared = []
for x in range(5):
    squared.append(x ** 2)

# 列表推导式(一行搞定)
squared = [x ** 2 for x in range(5)]

核心优势

  • 语法简洁,一行完成
  • 比显式循环快 5-10 倍
  • 是 Python 最地道(Pythonic)的写法

第三部分:while 循环

while 循环在条件为真时重复执行:

condition = True  # 设置一次可终止的演示条件
while condition:
    # 执行操作
    # (必须有使condition变False的逻辑,否则死循环)
    condition = False  # 更新条件并保证示例终止

金融应用:迭代求解(牛顿法、梯度下降)

\[ \large x_{n+1} = x_n - \frac{f(x_n)}{f'(x_n)} \]

案例:牛顿法求平方根

Listing 7: while循环实现牛顿法求平方根
展开完整参考实现
import numpy as np

def sqrt_newton(x, tolerance=1e-6, max_iter=100):
    """
    牛顿迭代法计算√2
    迭代公式: x_{n+1} = 0.5 * (x_n + S/x_n)
    """
    S = 2.0
    x_old = x

    print(f'牛顿法求√{S}:')
    print(f'{"迭代":<6}{"近似值":<15}{"误差":<15}')
    print('-' * 40)

    for i in range(max_iter):
        x_new = 0.5 * (x_old + S / x_old)  # 牛顿迭代
        error = abs(x_new - x_old)           # 绝对误差
        print(f'{i+1:<6}{x_new:<15.10f}{error:<15.10f}')

        if error < tolerance:                # 检查收敛
            print(f'\n收敛! 迭代{i+1}次后达到精度要求')
            break
        x_old = x_new
    else:
        print(f'\n警告: 未在{max_iter}次内收敛')

    return x_new

牛顿法:结果与误差核对

Listing 8
展开牛顿法结果核对
sqrt_approx = sqrt_newton(1.0)
sqrt_actual = np.sqrt(2)
print(f'\n近似值: {sqrt_approx:.10f}')
print(f'实际值: {sqrt_actual:.10f}')
print(f'误差: {abs(sqrt_approx - sqrt_actual):.2e}')
牛顿法求√2.0:
迭代    近似值            误差             
----------------------------------------
1     1.5000000000   0.5000000000   
2     1.4166666667   0.0833333333   
3     1.4142156863   0.0024509804   
4     1.4142135624   0.0000021239   
5     1.4142135624   0.0000000000   

收敛! 迭代5次后达到精度要求

近似值: 1.4142135624
实际值: 1.4142135624
误差: 2.22e-16

牛顿法的收敛特性

核心要点

  • 二阶收敛:每次迭代有效位数翻倍
  • 仅需数次迭代即可达到极高精度
  • 适合光滑函数求根

金融应用:隐含波动率

  • 已知期权市场价格,反推 Black-Scholes 中的波动率 \(\sigma\)
  • 这是无法用解析法求解的,必须用数值方法(如牛顿法)

金融应用:隐含波动率求解

课堂输入(非市场观测)\(S=100\)\(K=105\)\(T=1\)\(r=5\%\)、期权价格 \(8.02\);在 \(0.01\%\)\(300\%\) 内检查根并求解。

Listing 9: 有界求根计算隐含波动率
展开隐含波动率迭代代码
import numpy as np  # 提供 Black-Scholes 公式所需的对数和平方根运算
from scipy.optimize import brentq  # 调用保持在给定区间内的 Brent 有界求根器
from scipy.stats import norm  # 计算标准正态分布累积概率
def black_scholes_call_price(spot_price, strike_price, time_years, risk_free_rate, volatility):  # 计算不分红欧式看涨期权理论价格
    d1 = (np.log(spot_price / strike_price) + (risk_free_rate + 0.5 * volatility**2) * time_years) / (volatility * np.sqrt(time_years))  # 标准化到期价格阈值
    d2 = d1 - volatility * np.sqrt(time_years)  # 将 d1 调整为行权概率阈值
    return spot_price * norm.cdf(d1) - strike_price * np.exp(-risk_free_rate * time_years) * norm.cdf(d2)  # 返回 Black-Scholes 看涨期权价格
def solve_implied_volatility(option_price, spot_price=100, strike_price=105, time_years=1, risk_free_rate=0.05, lower_bound=0.0001, upper_bound=3):  # 在给定波动率边界内反解隐含波动率
    lower_error = black_scholes_call_price(spot_price, strike_price, time_years, risk_free_rate, lower_bound) - option_price  # 计算下界价格误差
    upper_error = black_scholes_call_price(spot_price, strike_price, time_years, risk_free_rate, upper_bound) - option_price  # 计算上界价格误差
    if lower_error * upper_error > 0:  # 检查两端误差是否异号以确认区间内存在根
        return None, '失败:期权价格超出求解边界'  # 明确返回失败状态而不产生无效波动率
    pricing_error = lambda volatility: black_scholes_call_price(spot_price, strike_price, time_years, risk_free_rate, volatility) - option_price  # 定义理论价与输入价的差额函数
    bounded_volatility = brentq(pricing_error, lower_bound, upper_bound, xtol=1e-8)  # 在有效区间内定位价格误差的根
    return bounded_volatility, '求解成功'  # 返回有界解与明确状态
teaching_option_price = 8.02  # 设置非市场观测的确定性课堂价格输入
estimated_volatility, solver_status = solve_implied_volatility(teaching_option_price)  # 调用有界求根器并接收状态
print('课堂输入(非市场观测): S=100, K=105, T=1, r=5%, 价格=8.02')  # 披露参数性质与完整输入
if estimated_volatility is None:  # 根据边界检查结果分流成功与失败状态
    print(solver_status)  # 在无根时显示明确失败原因
else:  # 在找到区间根时执行价格核对
    reconstructed_price = black_scholes_call_price(100, 105, 1, 0.05, estimated_volatility)  # 用反解波动率重新计算理论价
    print(f'{solver_status}: IV={estimated_volatility:.2%} | 核对价={reconstructed_price:.4f} | 残差={abs(reconstructed_price - teaching_option_price):.2e}')  # 紧凑呈现解、核对价与定价残差
课堂输入(非市场观测): S=100, K=105, T=1, r=5%, 价格=8.02
求解成功: IV=20.00% | 核对价=8.0200 | 残差=1.69e-11

循环控制语句:break 与 continue

语句 作用 类比
break 立即退出整个循环 紧急刹车
continue 跳过本次迭代,继续下一次 跳过一站

break 演示:找到第一个正收益即停止

观察重点break 在首个正收益处结束;continue 跳过负收益后继续统计。

Listing 10: break与continue的演示
展开 break 与 continue 追踪代码
# ===== break演示 =====
print('break演示(找到第一个正收益):')
returns = [-0.02, 0.03, -0.01, 0.04, 0.01, -0.03, 0.02]

for i, ret in enumerate(returns):
    if ret > 0:
        print(f'  第{i+1}天: {ret:.2%}')
        break  # 找到后立即退出

# ===== continue演示 =====
print('\ncontinue演示(跳过负收益):')
positive_count = 0
for ret in returns:
    if ret < 0:
        continue  # 负收益直接跳过
    positive_count += 1
    print(f'  正收益: {ret:.2%}')
print(f'\n正收益天数: {positive_count}天')
break演示(找到第一个正收益):
  第2天: 3.00%

continue演示(跳过负收益):
  正收益: 3.00%
  正收益: 4.00%
  正收益: 1.00%
  正收益: 2.00%

正收益天数: 4天

嵌套循环与 for-else 结构

观察重点:内层循环识别对角线,每轮正常结束后执行 else

Listing 11: 嵌套循环与for-else结构
展开嵌套循环追踪代码
print('嵌套循环演示(2×2矩阵):')  # 打印演示标题并控制输出高度
for i in range(2):  # 外层循环遍历行号
    for j in range(2):  # 内层循环遍历列号
        if i == j:  # 行列相等即为对角线元素
            print(f'  ({i}, {j}): 对角线元素')  # 输出对角线位置标识
        else:
            continue  # 非对角线跳过,保持循环继续
    # for-else:循环正常结束(未被break中断)时执行
    else:
        print(f'第{i}行完成')
嵌套循环演示(2×2矩阵):
  (0, 0): 对角线元素
第0行完成
  (1, 1): 对角线元素
第1行完成

for-else 规则:

  • else 块在循环正常完成时执行
  • 如果循环被 break 中断,else不会执行

控制流基础练习

  • 问题 1|需要准备哪些内容?:税率边界、价格状态和确定的迭代序列。
  • 问题 2|应得到哪些结果?:互斥分支真值表、for/while 运行记录、break/continue 命中位置。
  • 问题 3|哪些情况需要重新检查?:while 没有更新状态、分支重叠或边界遗漏时,先修正问题再继续;本页不要求使用函数、递归、作用域或 lambda。
  • 学习说明:完成本页后可结束控制流复习;以下内容等学完第 40 章函数基础后再选学。

选学扩展(第40章后):函数高级应用

可变参数 *args

*args 收集所有位置参数为元组,使函数能接受任意数量的参数:

def func(a, b, *args, **kwargs):
    # a=1, b=2
    # args=(3, 4)         ← 多余位置参数
    # kwargs={'x':5}      ← 关键字参数
    pass  # 此处留给学习者填写参数处理逻辑

func(1, 2, 3, 4, x=5)

案例:现金流现值计算

折现公式

\[ \large PV = \sum_{t=1}^{n} \frac{CF_t}{(1+r)^t} \]

  • \(CF_t\):第 \(t\) 期现金流
  • \(r\):贴现率
  • 口径:期初投资流出发生在 \(t=0\)、不折现;其余现金流自第 \(t=1\) 期起逐期折现
  • NPV > 0 → 项目创造价值;NPV < 0 → 不应投资

现值计算函数(使用 *args)

观察重点*args 接收任意期数的现金流,循环逐期折现并累加。

Listing 12: 现值计算函数(可变参数)
展开完整参考实现
def PV(R, *NCF, verbose=False):
    """
    计算各期现金流现值合计
    PV = Σ(CF_t / (1+r)^t),t 自 1 起
    *NCF 接收任意数量的现金流
    """
    pv = 0
    n = 1
    for cf in NCF:
        discount_factor = 1 / (1 + R) ** n  # 第n期折现因子
        discounted_cf = cf * discount_factor  # 该期现金流现值
        pv += discounted_cf  # 逐期累加
        if verbose:
            print(f'  第{n}期: 现金流={cf:8.2f}, 折现因子={discount_factor:.6f}, 现值={discounted_cf:8.2f}')
        n += 1
    if verbose:
        print(f'现值总和: {pv:.2f}元')  # 输出折现合计供课堂核对
    return pv

现值函数:两种时间口径

Listing 13
展开现值口径示例
# 案例1:简单投资项目(区分两种口径)
pv1 = PV(0.05, -10000, 8000, 12000)  # 期初流出也被当作第1期现金流折现
npv_t0 = -10000 + 8000 / 1.05 + 12000 / 1.05 ** 2  # 标准口径:期初流出在t=0不折现、流入自t=1起
print(f'案例1: 全部自第1期折现的合计 {pv1:.2f}元;期初流出口径NPV {npv_t0:.2f}\n')  # 两种口径并列展示

# 案例2:多期项目(verbose展示逐期折现过程)
project_flows = [-50000, 15000, 20000, 25000]  # 四期课堂示例现金流
pv2 = PV(0.08, *project_flows, verbose=True)

# 案例3:债券定价(仅输出合计)
bond_flows = [50] * 9 + [1050]  # 10年期,票面5%,面值1000
pv_bond = PV(0.06, *bond_flows)
print(f'\n案例3: 债券现值 {pv_bond:.2f}元')  # 输出债券定价结果
案例1: 全部自第1期折现的合计 8098.48元;期初流出口径NPV 8503.40元

  第1期: 现金流=-50000.00, 折现因子=0.925926, 现值=-46296.30
  第2期: 现金流=15000.00, 折现因子=0.857339, 现值=12860.08
  第3期: 现金流=20000.00, 折现因子=0.793832, 现值=15876.64
  第4期: 现金流=25000.00, 折现因子=0.735030, 现值=18375.75
现值总和: 816.18元

案例3: 债券现值 926.40元

递归函数:函数调用自身

递归三要素

  1. 基准情形(Base Case):递归终止条件
  2. 递归关系(Recursive Relation):问题如何分解
  3. 收敛性:确保递归最终会终止

数学归纳法的程序化实现

\[ \large n! = n \times (n-1)! \]

\[ \large F(n) = F(n-1) + F(n-2) \]

案例:阶乘与斐波那契数列

观察重点:先命中基准情形,再沿调用链逐层回传结果。

Listing 14: 递归函数:阶乘与斐波那契
展开递归调用代码
import numpy as np

# ===== 阶乘 =====
def factorial(n):
    """n! = n × (n-1)!,基准: 0!=1, 1!=1"""
    if n <= 1:
        return 1
    return n * factorial(n - 1)

print('阶乘计算:')
for n in [1, 5, 10, 20]:
    print(f'{n}! = {factorial(n)}')

# ===== 斐波那契 =====
def fibonacci(n):
    """F(0)=0, F(1)=1, F(n)=F(n-1)+F(n-2)"""
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(f'\n斐波那契数列前10项:')
print([fibonacci(i) for i in range(10)])
阶乘计算:
1! = 1
5! = 120
10! = 3628800
20! = 2432902008176640000

斐波那契数列前10项:
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

案例:二叉树期权定价模型

观察重点:先计算终端价值,再按风险中性概率逐层回推至根节点。

Listing 15: 二叉树模型计算期权价格
展开二叉树回推代码
import numpy as np

def binomial_tree(S, K, T, r, sigma, n, call=True):
    """
    Cox-Ross-Rubinstein二叉树模型
    u = e^(σ√dt), d = 1/u, p = (e^(r·dt) - d) / (u - d)
    """
    dt = T / n
    u = np.exp(sigma * np.sqrt(dt))   # 上涨因子
    d = 1 / u                          # 下跌因子
    p = (np.exp(r * dt) - d) / (u - d) # 风险中性概率

    # 终端节点期权价值
    option_values = np.zeros(n + 1)
    for j in range(n + 1):
        S_T = S * (u ** j) * (d ** (n - j))
        if call:
            option_values[j] = max(S_T - K, 0)
        else:
            option_values[j] = max(K - S_T, 0)

    # 从终端向根回推
    for i in range(n - 1, -1, -1):
        for j in range(i + 1):
            option_values[j] = np.exp(-r * dt) * (
                p * option_values[j + 1] + (1 - p) * option_values[j]
            )
    return option_values[0]

# 注:生产环境中可使用n=100或更高步数以提升精度
option_price = binomial_tree(S=100, K=105, T=1, r=0.05, sigma=0.2, n=20)
print(f'二叉树模型期权价格: {option_price:.2f}元')
二叉树模型期权价格: 8.11元

递归的效率问题

方法 时间复杂度 说明
朴素递归 \(O(2^n)\) 指数级,大量重复计算
动态规划 \(O(n^2)\) / \(O(n)\) 避免重复,从底向上

最佳实践:金融应用中必须优化递归

  • 二叉树模型 → 使用迭代法(动态规划)
  • 斐波那契 → 底向上迭代或记忆化

变量作用域:LEGB 规则

Python 变量查找遵循 LEGB 层次:

层级 名称 范围
L (Local) 局部作用域 函数内
E (Enclosing) 闭包作用域 嵌套函数的外层
G (Global) 模块作用域 文件内
B (Built-in) 内置作用域 Python 内置

作用域代码演示

Listing 16: 变量作用域与global关键字
展开完整参考实现
x = 10  # 全局变量
y = 20

def func_local():
    """局部变量遮蔽全局变量"""
    x = 5  # 局部变量,不影响全局x
    print(f'函数内x: {x} (局部)')

def func_global():
    """使用global修改全局变量"""
    global y
    y = 50
    print(f'函数内y: {y} (修改后的全局)')

def func_enclosing():
    """闭包:内层函数访问外层变量"""
    z = 100
    def inner():
        return x + z  # x来自Global, z来自Enclosing
    return inner()

作用域输出轨迹

Listing 17
展开作用域输出轨迹
print('初始状态:')
print(f'全局x: {x}, 全局y: {y}')

func_local()
print(f'func_local()后, 全局x: {x} (未改变)')

func_global()
print(f'func_global()后, 全局y: {y} (已修改)')

print(f'闭包结果: {func_enclosing()}')
初始状态:
全局x: 10, 全局y: 20
函数内x: 5 (局部)
func_local()后, 全局x: 10 (未改变)
函数内y: 50 (修改后的全局)
func_global()后, 全局y: 50 (已修改)
闭包结果: 110

局部遮蔽反例

Listing 18
展开局部遮蔽反例
# 错误示例:忘记global
def modify_global_wrong():
    y = 30  # 创建局部变量,不影响全局y

print(f'\n错误示例测试:')
print(f'修改前全局y: {y}')
modify_global_wrong()
print(f'modify_global_wrong()后, 全局y: {y} (未修改,因为没有global)')

错误示例测试:
修改前全局y: 50
modify_global_wrong()后, 全局y: 50 (未修改,因为没有global)

作用域最佳实践

  • 尽量使用局部变量,通过参数传递数据
  • 避免 global(除非绝对必要),防止代码维护困难
  • 闭包适合回调函数、策略参数封装

第五部分:lambda 表达式

lambda 是匿名的单行函数:

lambda parameters: expression
特点 说明
匿名 不需要函数名
简洁 只能是单个表达式
函数式 可作为参数传递给高阶函数

lambda 基础与金融收益率计算

观察重点lambda 将单个表达式封装为函数,可直接用于收益率与组合净值计算。

Listing 19: lambda表达式在金融计算中的应用
展开 lambda 金融计算代码
# 基本lambda函数
square = lambda x: x**2
cube = lambda x: x**3
print('lambda函数演示:')
print(f'平方: {square(5)} = {square(5)}')
print(f'立方: {cube(5)} = {cube(5)}')

# 金融收益率计算
simple_return = lambda initial, final: (final - initial) / initial
compound_return = lambda initial, rate, periods: initial * (1 + rate) ** periods - initial

print(f'\n简单收益率: {simple_return(100, 110):.2%}')
print(f'复利收益: {compound_return(10000, 0.05, 10):.2f}')

# 投资组合净值
net_value = lambda prices, shares: sum(p * s for p, s in zip(prices, shares))
prices = [10.5, 22.3, 15.8]
shares = [100, 200, 150]
print(f'\n投资组合净值: {net_value(prices, shares):.2f}元')
lambda函数演示:
平方: 25 = 25
立方: 125 = 125

简单收益率: 10.00%
复利收益: 6288.95

投资组合净值: 7880.00元

lambda 与高阶函数:map、filter、sorted

观察重点map 做变换,filter 做筛选,sorted 按键函数排序。

Listing 20: lambda与高阶函数配合使用
展开高阶函数组合代码
stocks = ['贵州茅台', '五粮液', '招商银行']
prices = [1850, 220, 45]

# map:对每个元素应用函数
formatted = list(map(lambda x: f'{x}元', prices))
print(f'格式化价格: {formatted}')

# filter:过滤符合条件的元素
expensive = list(filter(lambda p: p[1] > 200, zip(stocks, prices)))
print(f'高价股票: {[s + "=" + str(p) + "元" for s, p in expensive]}')

# sorted:自定义排序
portfolio = [
    {'name': '贵州茅台', 'return': 0.15},
    {'name': '五粮液', 'return': 0.08},
    {'name': '招商银行', 'return': 0.12}
]
sorted_portfolio = sorted(portfolio, key=lambda x: x['return'], reverse=True)
print(f'\n按收益率排序:')
for stock in sorted_portfolio:
    print(f"  {stock['name']}: {stock['return']:.2%}")
格式化价格: ['1850元', '220元', '45元']
高价股票: ['贵州茅台=1850元', '五粮液=220元']

按收益率排序:
  贵州茅台: 15.00%
  招商银行: 12.00%
  五粮液: 8.00%

lambda 与条件表达式生成中性状态

Listing 21: lambda条件表达式生成中性状态
# 嵌套三元表达式
get_signal = lambda r: 'ABOVE_ASSUMED_THRESHOLD' if r > 0.05 else ('ALERT' if r < -0.02 else 'REVIEW')

returns = [0.06, -0.03, 0.02, -0.01, 0.08]
signals = [get_signal(r) for r in returns]

print('课堂状态:')
for r, s in zip(returns, signals):
    print(f'  收益率={r:6.2%}: 信号={s}')
课堂状态:
  收益率= 6.00%: 信号=ABOVE_ASSUMED_THRESHOLD
  收益率=-3.00%: 信号=ALERT
  收益率= 2.00%: 信号=REVIEW
  收益率=-1.00%: 信号=REVIEW
  收益率= 8.00%: 信号=ABOVE_ASSUMED_THRESHOLD

lambda 使用建议

场景 推荐 原因
简单单行逻辑 ✅ lambda 简洁高效
高阶函数参数 ✅ lambda 避免定义独立函数
复杂多行逻辑 ❌ 用 def 可读性优先
需要文档字符串 ❌ 用 def lambda 无法添加 docstring

本章核心总结

控制结构知识总结 本章控制结构与函数高级应用的知识体系总结 Python 控制结构 选择: if-elif-else 条件判断 · 分支逻辑 税率计算 · 交易信号 循环: for / while 迭代遍历 · 数值求解 复利计算 · 牛顿法 函数: 递归 · lambda *args · 作用域 · 闭包 期权定价 · 现值计算 控制语句: break(退出) · continue(跳过) · for-else 应用:交易规则 · 风险度量 · 现值计算

运行前预测|平台任务1:工资扣税计算

  • 输入预测:运行前先写出 salaryrate 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。
  • 结果预测:不展开答案,先预测将得到“税后工资为:%d”相关结果;同时写出方向、数量级或表格/图形结构。
  • 完成要求:能独立说明本任务从输入到“平台任务1:工资扣税计算”结果的关键步骤,原样录入平台代码并得到可核对的运行结果。

⭐ 平台任务1:工资扣税计算

# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
#题目一
salary = 8000  # 基本工资
if salary <= 5000:  # 判断基本工资是否小于等于5000
    rate = 0  # 基本工资小于等于5000,不扣税,即扣税率为0%
else:  # 不满足以上条件时
    rate = 0.05  # 基本工资大于5000,扣税率为0.05,即5%

# 计算税后工资,基本工资-扣税,扣税额为超过3000部分的乘以扣税率
salary = salary - (salary - 5000) * rate
print("税后工资为:%d" % salary)  # 将税后工资打印输出

任务复盘|平台任务1:工资扣税计算

运行后核对:核对 salaryrate 是否按预测参与运算,实际输出是否与预测一致;若不一致,先检查类型、单位、索引/字段和运算顺序。

拓展练习:只改变一个关键输入或业务场景,先预测输出如何变化,再运行验证并解释变化原因。

运行前预测|平台任务2:员工薪资统计

  • 输入预测:运行前先写出 sum_salarysalary_listsalary 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。
  • 结果预测:不展开答案,先预测将得到“程序结束”相关结果;同时写出方向、数量级或表格/图形结构。
  • 完成要求:能独立说明本任务从输入到“平台任务2:员工薪资统计”结果的关键步骤,原样录入平台代码并得到可核对的运行结果。

⭐ 平台任务2:员工薪资统计

展开完整代码(投影默认折叠)
# 注:该代码块包含input()交互输入,渲染时无法执行
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
#题目二
sum_salary = 0
salary_list = []  # 定义列表salary_list
salary = 0  # 初始化薪资变量为0
while True:  # 条件循环
  salary = input('请输入员工的薪资,输入Q结束计算:')  # 获取用户键盘输入
  if salary.upper() == 'Q':  # 条件判断:salary.upper() == 'Q'
    print('程序结束')  # 输出程序结束
    break  # 跳出循环
  elif int(salary) <= 0:  # 否则判断:int(salary) <= 0
    print('您输入的数值有误,请重新输入')  # 输出您输入的数值有误,请重新输入
    continue  # 跳过本次迭代
    salary_list.append(salary)  # 将有效薪资数据添加到列表
  sum_salary += int(salary)  # 更新sum_salary的值
print(len(salary_list))  # 输出数据长度
print(sum_salary)  # 输出薪资数据

任务复盘|平台任务2:员工薪资统计

运行后核对:核对 sum_salarysalary_listsalary 是否按预测参与运算,实际输出是否与预测一致;若不一致,先检查类型、单位、索引/字段和运算顺序。

拓展练习:把输入表替换为本地中国上市公司数据的同结构子集;指出必须保持的字段、数据类型和质量检查。

运行前预测|平台任务3:投资组合期望报酬率

  • 输入预测:运行前先写出 RiAiTRTRij 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。
  • 结果预测:不展开答案,先预测将得到“投资组合的期望报酬率为:{:.2f}”相关结果;同时写出方向、数量级或表格/图形结构。
  • 完成要求:说明平台填空题的思路并按平台要求录入。受保护代码中的缩进按平台原文保留;本地学习时,应另用语法正确的代码理解计算过程。

⭐ 平台任务3:投资组合期望报酬率

# 注:该代码块包含缩进错误的填空代码,需要在平台上完成
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
#题目三
Ri=[0.15,0.20,-0.10,0.35]        #收益率序列
Ai=[0.3,0.2,0.3,0.2]             #权重序列
TR=0  # 初始化投资组合期望报酬率为0
for j in range(len(Ri)):  # 遍历range(len(Ri))中的每个j
        TRij=Ri[j]*Ai[j]  # 计算第j项资产的加权收益贡献(收益率×权重)
    TR+=TRij  # 更新TR的值
print("投资组合的期望报酬率为:{:.2f}".format(TR))  # 输出投资组合的期望报酬率为:{:.2f}

平台代码与本地练习的区别

  • TRij 比循环体多缩进,而 TR += TRij 又少缩进;标准 Python 会报 IndentationError,不会产生投资组合结果。
  • 保护块只能按平台要求原文写出。参考实现需先检查收益率与权重长度、有限值和权重和,再进行逐项加权。
展开保护块之外的参考实现
import numpy as np  # 导入数值工具执行有限值与权重检查
def portfolio_expected_return(return_rates, weights):  # 定义可核对的组合收益接口
    returns = np.asarray(return_rates, dtype=float)  # 转换收益率输入
    allocation = np.asarray(weights, dtype=float)  # 转换权重输入
    if returns.shape != allocation.shape or returns.ndim != 1:  # 检查一维长度一致
        raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 形状不匹配时停止
    if not np.isfinite(returns).all() or not np.isfinite(allocation).all():  # 检查有限值
        raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 非有限值时停止
    if not np.isclose(allocation.sum(), 1.0):  # 检查权重和
        raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 权重口径不合格时停止
    return float(np.dot(returns, allocation))  # 返回组合期望收益率

任务复盘|平台任务3:投资组合期望报酬率

运行后核对:核对 RiAiTRTRij 是否按预测参与运算,实际输出是否与预测一致;若不一致,先检查类型、单位、索引/字段和运算顺序。

拓展练习:只改变一个关键输入或业务场景,先预测输出如何变化,再运行验证并解释变化原因。

随堂练习

  • 问题 1|需要准备哪些数据?:税率边界、价格信号和迭代序列。
  • 问题 2|需要完成哪些操作?:运行 lst-if-salary-taxlst-break-continue,形成分支跟踪和循环终止记录。
  • 问题 3|应得到哪些结果?:边界真值表、逐步输出、break/continue命中位置。
  • 问题 4|怎样确认结果可靠?:测试边界两侧与等号点;while无更新或分支重叠时先检查原因。
  • 问题 5|换一个情境,怎样继续应用?:把控制流拓展应用到中国授信审批规则。
  • 作答提示:请依次写清所用数据、分析过程、所得结果、核对方法和拓展思考。课程所需数据见前言中的下载入口;教学平台固定题按页面说明完成。

教师参考解答|答案与说明 1

  • 所用数据与字段:确定性涨跌幅列表 float;课堂阈值 −3%/3%,只用于语法演示。

教师参考解答|代码 1

展开代码(代码区可独立滚动)
changes=[-.04,-.03,0,.03,.05]; labels=[]  # 生成changes以承接当前分析步骤
for change in changes:  # 计算相邻年度指标的百分点变化
    if change < -.03: labels.append('下跌核对')  # 计算相邻年度指标的百分点变化
    elif change > .03: labels.append('上涨核对')  # 计算相邻年度指标的百分点变化
    else: labels.append('区间内')  # 把本轮计算结果追加到有序检查列表
i=0; trace=[]  # 生成i以承接当前分析步骤
while i<len(changes):  # 在显式终止条件下逐轮更新状态轨迹
    trace.append((i,changes[i])); i+=1  # 把本轮计算结果追加到有序检查列表
assert labels==['下跌核对','区间内','区间内','区间内','上涨核对']  # 检查labels满足预声明的等式或边界
print(labels,trace)  # 输出labels、trace供核对结果

教师参考解答|答案与说明 2

  • 解释答案:if 负责互斥分支,for 适合已知序列遍历,while 适合由状态决定的迭代;每条路径都要有可观察状态和明确需要重新检查的情况。
  • 拓展应用答案:拓展应用到额度预警时,逐笔遍历余额、按阈值分支并记录轨迹;用最大迭代数和余额不再变化作为双重需要重新检查的情况。
  • 参考结果:等号点均落入“区间内”,循环5次。另行核对:把输入反序,分支计数不变、遍历顺序改变;另用最小列表验证 continue 跳过而 break 终止。
  • 常见错误:漏写终止更新;混淆 <<=;把课堂阈值写成交易规则。